上一篇講到巢狀資料:
data?.customer?.name
也提到一個很常見的情況:
API 回來的資料結構,不一定剛好就是 UI Component 需要的格式。
這時候前端常常會做一件事:
map()
以前我很容易把 map() 記成:
「一種迴圈。」
但實際進專案後才慢慢發現:
map()更重要的用途,是把一份 Array 轉換成另一份新的 Array。
假設:
const numbers = [1, 2, 3];
我們想把每個數字乘以 2:
const result = numbers.map(number => {
return number * 2;
});
結果:
[2, 4, 6]
流程:
原本 Array
[1, 2, 3]
↓ map()
1 → 2
2 → 4
3 → 6
↓ 新 Array
[2, 4, 6]
所以 map() 的重點不是單純:
每一筆都跑一次。
而是:
每一筆經過處理後,產生一個新的結果,最後組成新的 Array。
假設 API 回來:
const customers = [
{
id: 1,
customer: {
name: "ABC Company",
},
},
{
id: 2,
customer: {
name: "XYZ Company",
},
},
];
但今天 Select 元件需要的是:
[
{
label: "ABC Company",
value: 1,
},
{
label: "XYZ Company",
value: 2,
},
];
兩份資料其實有同樣的內容。
只是:
形狀不同。
API 給的是:
id
customer
└─ name
Select 想要的是:
label
value
這時候就需要轉換。
map() 把 API 資料整理成 UI 要的格式例如:
const options = customers.map(item => ({
label: item.customer?.name ?? "N/A",
value: item.id,
}));
最後:
options
會變成:
[
{
label: "ABC Company",
value: 1,
},
{
label: "XYZ Company",
value: 2,
},
];
流程:
API Response
↓
customers
每一筆 item
↓
取 item.customer.name
取 item.id
↓
重新組成
{
label,
value
}
↓
新的 Array
↓
Select options
這就是我後來理解 map() 最有感的地方。
item => ({ ... }) 又是在幹嘛?這段:
customers.map(item => ({
label: item.customer?.name ?? "N/A",
value: item.id,
}));
第一次看很像一坨。
可以先拆成完整版本:
const options = customers.map(item => {
return {
label: item.customer?.name ?? "N/A",
value: item.id,
};
});
流程:
拿到一筆 item
↓
return 一個新的 Object
↓
下一筆
↓
再 return 新 Object
↓
最後全部組成 Array
所以:
item
是:
原本 Array 裡目前正在處理的那一筆資料。
而:
return {
label: ...,
value: ...,
}
就是:
這一筆轉換後,我希望它長什麼樣子。
{}?這裡還有一個很容易讓我混亂的語法:
item => ({
label: item.name,
value: item.id,
})
為什麼 Object 外面要多一層:
(...)
因為箭頭函式如果直接寫:
item => {
...
}
JavaScript 會把 {} 理解成 function body。
所以如果想直接 return Object:
item => ({
...
})
就是用括號告訴 JavaScript:
這整包
{}是我要回傳的 Object。
等同:
item => {
return {
...
};
}
map() 不會直接改原本的 Array例如:
const customers = [
{
id: 1,
name: "ABC",
},
];
接著:
const options = customers.map(item => ({
label: item.name,
value: item.id,
}));
此時:
customers
仍然是:
[
{
id: 1,
name: "ABC",
},
]
而:
options
才是新的:
[
{
label: "ABC",
value: 1,
},
]
所以:
customers
→ 原始資料
options
→ map() 後的新資料
這也是 map() 很重要的特性:
它會回傳新的 Array。
forEach() 有什麼差別?這也是我以前很容易混在一起的地方。
假設:
const numbers = [1, 2, 3];
map()const result = numbers.map(number => {
return number * 2;
});
得到:
[2, 4, 6]
因為 map() 的目的通常是:
把每一筆轉換後,產生新的 Array。
forEach()numbers.forEach(number => {
console.log(number);
});
這種比較像:
每一筆都幫我做某件事。
例如:
印 log
修改 DOM
呼叫 function
但它不會像 map() 一樣,自動幫我們收集成新的 Array。
所以可以先簡單分:
map()
→ 我想把 Array 轉成另一個 Array
forEach()
→ 我想對每一筆執行某件事
map()?上一篇以前其實就看過:
memos.map(memo => (
<li>{memo}</li>
))
這裡本質上也是一樣的。
原本:
["買牛奶", "寫文章"]
經過:
map()
變成:
[
<li>買牛奶</li>,
<li>寫文章</li>,
]
也就是:
資料 Array
↓
map()
↓
React Element Array
↓
Render
所以 React 並不是賦予 map() 什麼特殊能力。
map() 本來就是 JavaScript Array 的方法。
React 只是很常拿它來:
把資料轉成畫面。
假設 API 回:
[
{
id: 1,
customer: {
name: "ABC",
},
amount: 1000,
}
]
但畫面希望使用:
[
{
key: 1,
customerName: "ABC",
displayAmount: "$1,000",
}
]
也可能:
const tableData = data.map(item => ({
key: item.id,
customerName:
item.customer?.name ?? "N/A",
displayAmount:
`$${item.amount}`,
}));
所以資料流是:
API Response
↓
原始資料
↓
map()
↓
整理欄位
↓
UI Data
↓
Table
這種「資料轉換層」在前端其實非常常見。
map()這也是我後來慢慢學到的。
假設 API 已經回:
[
{
label: "ABC",
value: 1,
}
]
而 Select 剛好就需要:
label
value
那根本不用再:
map()
重新做一份一模一樣的資料。
所以要先問:
API 的資料形狀,跟 UI 需要的形狀差在哪裡?
真的需要轉換,才轉。
map(),會先問三件事以前:
map()又是什麼語法?
現在我會先問:
① 原本 Array 長什麼樣子?
② 每一筆被轉成什麼?
③ 最後的新 Array 要給誰使用?
例如:
const options = customers.map(item => ({
label: item.customer?.name ?? "N/A",
value: item.id,
}));
就變成:
原始資料
customers
↓ map()
每一筆:
{
id,
customer
}
↓ 轉換成
{
label,
value
}
↓ 最後
options
↓
Select
只要找到這三件事,map() 就不會再只是一個抽象的「迴圈」。
API 關心的可能是:
資料庫 / Domain 的資料結構
UI Component 關心的可能是:
我要 label / value
所以 Frontend 中間常常需要做:
API Data
↓
Transform
↓
UI Data
而 map() 就是這種資料轉換裡很常見的工具。
所以我現在看到:
map()
不會只想:
它在跑迴圈。
而會多問一句:
它正在把什麼資料,轉成什麼形狀?
下一篇可以繼續接資料轉換裡另一個非常常見的問題:
為什麼有時候要用 { ...data }、[...items]?Spread Operator 到底是在「展開」什麼?